MotivationSpring currently does not support session scoped beans/components out of the box. You can decide between singleton or prototype lifecycle, but you cannot have your beans bound to the session lifecycle of web applications. There are plans for integrating such a feature in the Spring 2.0 release.
General Solutions for WebapplicationsThe Spring 2.0 WayInterface21 added support for session (and request) scoped beans in Spring 2.0. This approach creates a CGLIB or JDK Dynamic proxy of the session scoped bean using the org.springframework.aop.target.scope.ScopedProxyFactoryBean and setting the scopeMap to org.springframework.web.context.scope.SessionScopeMap. Since the jars are backwards compatible simply build Spring and replace the jars shipped with WebWork. (Spring 2.0 M1 should be out by the time you read this.). There are 2 ways to set this up depending upon whether or not XML simplification is used. The first method uses the traditional bean definitions and is useful to understand what is happening under the covers. A modified applicationContext.xml for the shopping cart example using the traditional XML DTD is below. <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd"> <beans> <bean id="shoppingCart" class="org.springframework.aop.target.scope.ScopedProxyFactoryBean" singleton="false"> <property name="scopeKey" value="shoppingCart"/> <property name="targetBeanName" value="__shoppingCart"/> <property name="scopeMap"> <bean class="org.springframework.web.context.scope.SessionScopeMap"/> </property> </bean> <bean id="__shoppingCart" class="com.opensymphony.webwork.example.ajax.cart.DefaultCart" singleton="false"/> </beans> A modified applicationContext.xml for the shopping cart example using the XML simplification is below. <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <bean id="catalog" class="com.opensymphony.webwork.example.ajax.catalog.TestCatalog" singleton="true"/> <bean id="shoppingCart" class="com.opensymphony.webwork.example.ajax.cart.DefaultCart" singleton="true"> <aop:scope type="session"/> </bean> </beans> You will also need to modify the web.xml to include the following filter. <filter> <filter-name>springFilter</filter-name> <filter-class>org.springframework.web.filter.RequestContextFilter</filter-class> </filter> <filter-mapping> <filter-name>springFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> Custom TargetSource with a ServletFilterA quite "clean" solution for web applications in general can be found at JA-SIG. The solution is well documented and can be found here. Below you will find a WebWork adoption of this solution. XWork/WebWork specific solutionsPrefaceWebWork is based on XWork, and XWork is not tied to the web layer. So when dealing with session scoped objects, WW users might want to use XWork's session abstraction features to keep their application independent from the web context. This is why we will discuss some XW/WW specific solutions below. Custom TargetSource, the WebWork wayHere is a modified version of the TargetSource solution pointed out above that integrates with the existing WebWork session and doesn't require an additional filter or listener. Usage is pretty much the same, create an interface for your object and make sure that you always use that interface and not the underlying implementation or autowiring will fail. You can find more information on how to make this work by looking at the WebWorkTargetSource Shopping Cart Example. WebWorkTargetSource.java package org.tuxbot.webwork.spring; /* Portions Copyright 2005 The JA-SIG Collaborative. All rights reserved. * See license distributed with this file and * available online at http://www.uportal.org/license.html */ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.aop.target.AbstractPrototypeBasedTargetSource; import org.springframework.beans.factory.DisposableBean; import com.opensymphony.xwork.ActionContext; import java.util.Map; /** * This target source is to be used in collaberation with WebWork. * The target source binds the target bean to the Session retrieved from * WebWork. By default the bean is bound to the session * using the name of the target bean as part of the key. This can be overridden by setting * the <code>sessionKey</code> property to a not null value. * * @author Eric Dalquist <a href="mailto:[email protected]">[email protected]</a> * @author Eric Molitor <a href="mailto:[email protected]">[email protected]</a> * @version 1.0 */ public class WebWorkTargetSource extends AbstractPrototypeBasedTargetSource implements DisposableBean { private final static Log LOG = LogFactory.getLog(WebWorkTargetSource.class); private transient Object noSessionInstance = null; private String sessionKey = null; private String compiledSessionKey = null; public WebWorkTargetSource() { this.updateBeanKey(); } /** * @return Returns the sessionKey. */ public String getSessionKey() { return this.sessionKey; } /** * @param sessionKey The sessionKey to set. */ public void setSessionKey(String sessionKey) { this.sessionKey = sessionKey; this.updateBeanKey(); } /** * @see org.springframework.aop.target.AbstractBeanFactoryBasedTargetSource#setTargetBeanName(java.lang.String) */ public void setTargetBeanName(String targetBeanName) { super.setTargetBeanName(targetBeanName); this.updateBeanKey(); } /** * @see org.springframework.aop.TargetSource#getTarget() */ public Object getTarget() throws Exception { final Map session = ActionContext.getContext().getSession(); if (session == null) { LOG.warn("No Session found for thread '" + Thread.currentThread().getName() + "'"); if (this.noSessionInstance == null) { this.noSessionInstance = this.newPrototypeInstance(); if (LOG.isDebugEnabled()) { LOG.debug("Created instance of '" + this.getTargetBeanName() + "', not bound to any webWorkSession."); } } else { if (LOG.isDebugEnabled()) { LOG.debug("Found instance of '" + this.getTargetBeanName() + "', not bound to any webWorkSession."); } } return this.noSessionInstance; } else { String beanKey = this.compiledSessionKey; Object instance = session.get(beanKey); if (instance == null) { instance = this.newPrototypeInstance(); session.put(beanKey, instance); if (LOG.isDebugEnabled()) { LOG.debug("Created instance of '" + this.getTargetBeanName() + "', bound to webWorkSession for '" + Thread.currentThread().getName() + "' using key '" + beanKey + "'."); } } else if (LOG.isDebugEnabled()) { LOG.debug("Found instance of '" + this.getTargetBeanName() + "', bound to webWorkSession for '" + Thread.currentThread().getName() + "' using key '" + beanKey + "'."); } return instance; } } /** * @see org.springframework.beans.factory.DisposableBean#destroy() */ public void destroy() throws Exception { if (this.noSessionInstance != null && this.noSessionInstance instanceof DisposableBean) { if (LOG.isDebugEnabled()) { LOG.debug("Destroying sessionless bean instance '" + this.noSessionInstance + "'"); } ((DisposableBean)this.noSessionInstance).destroy(); } } /** * Generates the key to store the bean in the session with. */ private void updateBeanKey() { if (this.sessionKey == null) { final StringBuffer buff = new StringBuffer(); buff.append(this.getClass().getName()); buff.append("_"); buff.append(this.getTargetBeanName()); this.compiledSessionKey = buff.toString(); } else { this.compiledSessionKey = this.sessionKey; } } } Customized ApplicationContext ImplementationTODO: Document Customized WW/XW ObjectFactoryTODO: Document Session backed Bean FactoryThe idea is to simply create a retrieve-or-create bean factory: SessionBackedBeanFactory.java package net.itneering.core.spring.session; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import com.opensymphony.xwork.ActionContext; import java.util.Map; import java.io.Serializable; /** * SessionBackedBeanFactory tries to lookup beans by name in XWork session. If not found, * it tries to instantiate new bean and attaches it to said session. * * @author <a href="mailto:[email protected]">Rene Gielen</a> */ public class SessionBackedBeanFactory implements Serializable, BeanFactoryAware { BeanFactory beanFactory = null; /** * Find a component by name in session scoped storage implementation. If not found, try to instantiate new one by * {@link org.springframework.beans.factory.BeanFactory#getBean(String)}. Then found component will be attached * to session store implementation. * * @param componentName * @return The requested component, if found. */ public Object getSessionComponent( String componentName ) { Object result = getSession().get(componentName); if ( result == null ) { result = beanFactory.getBean(componentName); storeComponent(componentName, result); } return result; } public void storeComponent(String componentName, Object component ) { getSession().put(componentName, component); } /** * Actual implementation of the session scoped storage Map. * Lookup {@link com.opensymphony.xwork.ActionContext#getSession()}. * * @return The Map for keeping session objects. */ public Map getSession() { return ActionContext.getContext().getSession(); } /** * Callback that supplies the owning factory to a bean instance. * <p>Invoked after population of normal bean properties but before an init * callback like InitializingBean's afterPropertiesSet or a custom init-method. * * @param beanFactory owning BeanFactory (may not be null). * The bean can immediately call methods on the factory. * * @throws org.springframework.beans.BeansException * in case of initialization errors * @see org.springframework.beans.factory.BeanInitializationException */ public void setBeanFactory( BeanFactory beanFactory ) throws BeansException { this.beanFactory = beanFactory; } } Example applicationContext setup (note that the session scoped bean has to be setup with singleton="false"): applicationContext.xml <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd"> <beans default-autowire="byName"> <bean id="sessionBeanProxy" class="net.itneering.core.spring.session.SessionBackedBeanFactory" singleton="true"/> <bean id="securityContextComponent" class="net.itneering.security.component.DefaultSecurityContextComponent" singleton="false" /> </beans> Example action use: SecurityAwareAction.java package net.itneering.xwork.action; import com.opensymphony.xwork.ActionSupport; import net.itneering.core.spring.session.SessionBackedBeanFactory; import net.itneering.security.component.DefaultSecurityContextComponent; /** * Simple sessionBeanProxy aware action. * * @author <a href="mailto:[email protected]">Rene Gielen</a> * @version $Revision: 1.1 $ */ public class SecurityAwareAction extends ActionSupport implements PrincipalAware { private static final Logger log = Logger.getLogger(SecurityAwareAction.class); protected SessionBackedBeanFactory sessionBeanProxy; /** * For Spring wiring usage. * * @param sessionBeanProxy The sessionBeanProxyto use. */ public void setSessionBeanProxy( SessionBackedBeanFactory sessionBeanProxy ) { this.sessionBeanProxy = sessionBeanProxy; } /** * Getter for actions security context. * * @return The securityContextComponent set by IoC. */ public SecurityContextComponent getSecurityContextComponent() { return sessionBeanProxy!= null ? sessionBeanProxy.getSessionComponent("securityContextComponent") : null; } /** * Get the current User Principal for this session. * * @return The User Principal. */ public UserEntity getPrincipal() { try { return getSecurityContextComponent().getPrincipal(); } catch ( NullPointerException e ) { return null; } } ... } For well known session scoped components, you might get more convenience by subclassing SessionBackedBeanFactory: SecurityAwareSessionBeanProxy.java package net.itneering.security.component; import net.itneering.core.spring.session.SessionBackedBeanFactory; /** * SecurityAwareSessionBeanProxy. * * @author <a href="mailto:[email protected]">Rene Gielen</a> */ public class SecurityAwareSessionBeanProxy extends SessionBackedBeanFactory { String securityContextComponentName = "securityContextComponent"; /** * Make component name configurable by spring setup */ public void setSecurityContextComponentName( String securityContextComponentName ) { this.securityContextComponentName = securityContextComponentName; } public SecurityContextComponent getSecurityContextComponent() { return (SecurityContextComponent) getSessionComponent(securityContextComponentName); } } Again example applicationContext setup: applicationContext2.xml <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd"> <beans default-autowire="byName"> <bean id="mySecurityContextComponent" class="net.itneering.security.component.DefaultSecurityContextComponent" singleton="false" /> <bean id="sessionBeanProxy" class="net.itneering.security.component.SecurityAwareSessionBeanProxy" singleton="true"> <property name="securityContextComponentName" value="mySecurityContextComponent" /> </bean> </beans> Example action use: SecurityAwareAction2.java package net.itneering.xwork.action; import com.opensymphony.xwork.ActionSupport; import net.itneering.security.component.SecurityAwareSessionBeanProxy; import net.itneering.security.component.DefaultSecurityContextComponent; /** * Simple sessionBeanProxy aware action. * * @author <a href="mailto:[email protected]">Rene Gielen</a> * @version $Revision: 1.1 $ */ public class SecurityAwareAction extends ActionSupport implements PrincipalAware { private static final Logger log = Logger.getLogger(SecurityAwareAction.class); protected SecurityAwareSessionBeanProxy sessionBeanProxy; /** * For Spring wiring usage. * * @param sessionBeanProxy The sessionBeanProxy to use. */ public void setSessionBeanProxy( SecurityAwareSessionBeanProxy sessionBeanProxy ) { this.sessionBeanProxy = sessionBeanProxy; } /** * Get the current User Principal for this session. * * @return The User Principal. */ public UserEntity getPrincipal() { try { return sessionBeanProxy.getSecurityContextComponent().getPrincipal(); } catch ( NullPointerException e ) { return null; } } ... } As said, the solution is very simple. You will get no ties to web layer, and the configuration is really simple, there is no need for proxy definitions in applicationContext.xml etc. Auto proxied Session backed Component FactoryDoes anyone have an implementation of this? (Eric Molitor) |